/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package cn.ac.iie.di.ban.pload;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.LinkedBlockingQueue;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;

/**
 *
 * @author zwlin
 */
public class PLoader {

    private static final String VERSION = "J0.7.1";
    private static final HashMap<String, PLoader> senderMap = new HashMap<>();
    private static final MD5Pool pool;

    static {
        pool = MD5Pool.getInstance();
    }

    static public PLoader getInstance(String key) throws
            NoSuchAlgorithmException,
            KeyStoreException,
            KeyManagementException,
            IOException,
            CertificateException,
            UnrecoverableKeyException {
        if (!senderMap.containsKey(key)) {
            synchronized (senderMap) {
                if (!senderMap.containsKey(key)) {
                    senderMap.put(key, new PLoader(key).start());
                }
            }
        }
        return senderMap.get(key);
    }

    static public void closeAll() {
        synchronized (senderMap) {
            for (PLoader sender : senderMap.values()) {
                try {
                    sender.close();
                } catch (Exception e) {
                }
            }
            senderMap.clear();
        }
    }

    /**
     * in test mode
     */
    private static String host = "";

    String key;
    CloseableHttpClient client;

    boolean isActive;
    private LinkedBlockingQueue<String> ips;
    int ipnum;
    int curip;
    Timer timer;
    long lastUpdateTime;

    Exception cur = null;

    private PLoader(String key) throws
            NoSuchAlgorithmException,
            KeyStoreException,
            KeyManagementException,
            IOException,
            CertificateException,
            UnrecoverableKeyException {
        this.key = key;

        //key manager
        KeyStore keyStore = KeyStore.getInstance("JKS");
        byte[] bs = Certs.getClientPrivatKeystoreBytes();
        ByteArrayInputStream bis = new ByteArrayInputStream(bs);
        keyStore.load(bis, "123456".toCharArray());

        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(keyStore, "123456".toCharArray());
        KeyManager[] km = kmf.getKeyManagers();

        //trust manager
        KeyStore trustStore = KeyStore.getInstance("JKS");
        bs = Certs.getServerPublicKeystoreBytes();
        bis = new ByteArrayInputStream(bs);
        trustStore.load(bis, "123456".toCharArray());

        TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
        tmf.init(trustStore);
        TrustManager[] tm = tmf.getTrustManagers();

        //ssl context
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(km, tm, null);
//        sslContext.init(km, tm4dgb, null);

        //client
        client = HttpClients.custom()
                .setSSLContext(sslContext)
                .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
                .build();

        isActive = false;
        ips = new LinkedBlockingQueue<>();
        timer = new Timer("ip_list_sync_timer");
        lastUpdateTime = 0;
        ipnum = 0;
    }

    public Status send(String Content) throws
            NoSuchAlgorithmException,
            InterruptedException,
            UnsupportedEncodingException,
            IOException,
            Exception {
        //check active
        if (!isActive) {
            throw new Exception("Sender is stopped.");
        }
        //Exception
        if (cur != null) {
            synchronized (this) {
                if (cur != null) {
                    Exception tmp = cur;
                    cur = null;
                    throw new Exception("exception occurs.", tmp);
                }
            }
        }
        //get ip
        if (ipnum == 0) {
            updateIps();
        }
        if (ipnum == 0) {
            throw new Exception("no valid data ip");
        }
        if (Content.length() > 150000) {
            Status status = new Status();
            status.reasonPhrase = "8. the json length too long > 150000 Byte";
            status.success = false;
            return status;
        }
        for (int i = 0; i < ipnum; ++i) {
            String ip = ips.take();
            ips.put(ip);

            try {
                //post
                HttpPost post = new HttpPost("https://" + ip + "/data/");
                List<NameValuePair> params = new ArrayList<>(3);
                params.add(new BasicNameValuePair("key", key));
                params.add(new BasicNameValuePair("md5", getMD5(Content)));
                params.add(new BasicNameValuePair("content", Content));
                params.add(new BasicNameValuePair("version", VERSION));
                UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(params, "UTF-8");
                post.setEntity(formEntity);
                CloseableHttpResponse response = client.execute(post);

                //status
                try {
                    Thread.sleep(1);
                } catch (Exception e) {
                }
                Status status = new Status();
                StatusLine statusLine = response.getStatusLine();
                int statusCode = statusLine.getStatusCode();
                InputStream is = response.getEntity().getContent();
                status.success = statusCode == 200;
//                if (!status.success) {
//                    System.out.println(statusCode);
//                }
                status.reasonPhrase = response.getStatusLine().getReasonPhrase();
//		if (response.getEntity().getContentLength() > 0) {
//			byte[] bs = new byte[(int) response.getEntity().getContentLength() + 1];
//			bs[bs.length - 1] = 0;
//			response.getEntity().getContent().read(bs);
//			status.reasonPhrase = new String(bs);
//		}
                try {
                    response.close();
                } catch (Exception e) {
                }
                return status;
            } catch (Exception e) {
            }
        }
        if (System.currentTimeMillis() - lastUpdateTime < 5000) {
            throw new Exception("no healthy send ip.");
        } else {
            updateIps();
            String hosts = ips.take();
            ips.put(hosts);

            //post
            HttpPost post = new HttpPost("https://" + hosts + "/data/");
            List<NameValuePair> params = new ArrayList<>(3);
            params.add(new BasicNameValuePair("key", key));
            params.add(new BasicNameValuePair("md5", getMD5(Content)));
            params.add(new BasicNameValuePair("content", Content));
            params.add(new BasicNameValuePair("version", VERSION));
            post.setEntity(new UrlEncodedFormEntity(params));
            CloseableHttpResponse response = client.execute(post);

            //status
            Status status = new Status();
            status.success = response.getStatusLine().getStatusCode() == 200;
            status.reasonPhrase = response.getStatusLine().getReasonPhrase();
//		if (response.getEntity().getContentLength() > 0) {
//			byte[] bs = new byte[(int) response.getEntity().getContentLength() + 1];
//			bs[bs.length - 1] = 0;
//			response.getEntity().getContent().read(bs);
//			status.reasonPhrase = new String(bs);
//		}
            try {
                response.close();
            } catch (Exception e) {
            }
            return status;
        }
    }

    public PLoader start() throws IOException {
        updateIps();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                updateIps();
            }
        }, 5 * 60 * 1000, 5 * 60 * 1000);
        isActive = true;
        return this;
    }

    public boolean isAlive() {
        return isActive;
    }

    public void close() throws IOException {
        isActive = false;
        timer.cancel();
        client.close();
    }

    public static String getMD5(String content) throws NoSuchAlgorithmException, UnsupportedEncodingException {
        MessageDigest md = pool.Borrow();
        try {
            byte[] bs = md.digest(content.getBytes("utf-8"));
            StringBuilder sb = new StringBuilder();
            int i;
            for (byte b : bs) {
                i = b & 0x00ff;
                if (i < 16) {
                    sb.append('0');
                }
                sb.append(Integer.toHexString(i));
            }
            return sb.toString();
        } finally {
            if (md != null) {
                pool.Return(md);
            }
        }
    }

    private synchronized void updateIps() {
        if (System.currentTimeMillis() - lastUpdateTime < 5000) {
            return;
        }
        for (int i = 0; i < 3; ++i) {
            try {
                List<String> gotIps = getIps();
                if (gotIps == null) {
                    continue;
                }
                LinkedBlockingQueue<String> newips = new LinkedBlockingQueue<>(gotIps);
                LinkedBlockingQueue<String> tmp = ips;
                ipnum = newips.size();
                ips = newips;
                tmp.clear();
                return;
            } catch (IOException ex) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ex1) {
                }
            }
        }
        try {
            List<String> gotIps = getIps();
            if (gotIps == null) {
                return;
            }
            LinkedBlockingQueue<String> newips = new LinkedBlockingQueue<>(gotIps);
            LinkedBlockingQueue<String> tmp = ips;
            ipnum = newips.size();
            ips = newips;
            tmp.clear();
        } catch (IOException ex) {
            PLoader.this.cur = new Exception("cannot update iplist.", ex);
        }
    }

    private static String[] dispatchers = {"123.125.45.210:19841", "123.125.45.210:19941"};

    private static int idx = 0;

    boolean isTest = false;
    String testDispatcher = null;

    private synchronized String getDispatcher() {
        if (isTest) {
            return testDispatcher;
        } else {
            if (idx < dispatchers.length) {
            } else {
                idx = 0;
            }
            return dispatchers[idx++];
        }
    }

    public void setTest(String dispatcherHost) {
        isTest = true;
        testDispatcher = dispatcherHost;
    }

    /**
     * in test mode
     *
     * @return
     * @throws IOException
     */
    private List<String> getIps() throws IOException {

//		HttpPost post = new HttpPost("https://" + InetAddress.getByName(host).toString());
        HttpPost post = new HttpPost("https://" + getDispatcher() + "/dispatch/");
//        HttpPost post = new HttpPost("https://127.0.0.1:10240/dispatch/");
        List<NameValuePair> params = new ArrayList<>(1);
        params.add(new BasicNameValuePair("key", key));
        params.add(new BasicNameValuePair("version", VERSION));
        post.setEntity(new UrlEncodedFormEntity(params));
        CloseableHttpResponse response = client.execute(post);
        if (response.getStatusLine().getStatusCode() != 200) {
            return null;
        }
        BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent()));
        String str;
        ArrayList<String> ips = new ArrayList<>();
        while ((str = br.readLine()) != null) {
            ips.add(str);
        }
        response.close();
//        ips.add("127.0.0.1:10240");
        return ips;
    }

    private static class MD5Pool {

        private static MD5Pool ins = null;

        public static MD5Pool getInstance() {
            if (ins == null) {
                synchronized (MD5Pool.class) {
                    if (ins == null) {
                        ins = new MD5Pool();
                    }
                }
            }
            return ins;
        }

        private MD5Pool() {
            pool = new LinkedBlockingQueue<>();
        }

        private LinkedBlockingQueue<MessageDigest> pool;

        public MessageDigest Borrow() throws NoSuchAlgorithmException {
            try {
                return pool.remove();
            } catch (Exception e) {
                return MessageDigest.getInstance("MD5");
            }
        }

        public void Return(MessageDigest md) {
            pool.add(md);
        }

    }
}
